using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading;

namespace DZTC.Base32Utility.Core.Base32
{
    public static class Base32Decoder
    {
        private static readonly byte[] DECODE_TABLE = CreateDecodeTable(Base32Encoder.ENCODE_TABLE);
        private const byte INVALID_CHARACTER = 0xff;

        private static byte[] CreateDecodeTable(byte[] encTable)
        {
            var table = new byte[256];
            for (int i = 0; i < 256; i++)
            {
                table[i] = INVALID_CHARACTER;
            }

            for (byte i = 0; i < encTable.Length; i++)
            {
                table[encTable[i]] = i;
            }

            return table;
        }

        private static int DecodeQuantumLength(int quantumLength)
        {
            Debug.Assert(0 <= quantumLength && quantumLength <= 8);

            switch (quantumLength)
            {
                case 8:
                    return 5;
                case 7:
                    return 4;

                case 5:
                    return 3;

                case 4:
                    return 2;

                case 2:
                    return 1;
            }

            // otherwise quantum is incorrect
            return -1;
        }



        private static int ReadByteSkippingCrLf(Stream stream)
        {
            while (true)
            {
                var b = stream.ReadByte();
                if (b != '\r' && b != '\n') return b;
            }
        }

        private static void DecodeQuantum(byte[] quantum, byte[] output)
        {
            Debug.Assert(quantum.Length == 8 && output.Length == 5);

            var number = ((long) quantum[0] << 35) + ((long) quantum[1] << 30) +
                         ((long) quantum[2] << 25) + ((long) quantum[3] << 20) +
                         ((long) quantum[4] << 15) + ((long) quantum[5] << 10) +
                         ((long) quantum[6] << 05) + ((long) quantum[7] << 00);

            output[0] = (byte) ((number >> 32) & 0xff);
            output[1] = (byte) ((number >> 24) & 0xff);
            output[2] = (byte) ((number >> 16) & 0xff);
            output[3] = (byte) ((number >> 08) & 0xff);
            output[4] = (byte) ((number >> 00) & 0xff);
        }

        private static void DecodeInternal(Stream inputStream, Stream outputStream,
                                           CancellationToken ct,
                                           ProgressReporter pr)
        {
            var quantum = new byte[8];
            var dquantum = new byte[5];

            var qpos = 0;
            var spos = 0L;

            pr.SetMax(inputStream.Length);

            while (true)
            {
                var b = ReadByteSkippingCrLf(inputStream);
                if (b == -1) break;

                var ch = (byte) b;
                if (ch == '=') // end?
                {
                    var dlen = DecodeQuantumLength(qpos);

                    // if quantum length is incorrect
                    if (dlen == -1) throw new InvalidPaddingException();

                    var padCount = 8 - qpos - 1; // 1 pad was already consumed 

                    // consume  pads 
                    for (var i = 0; i < padCount; i++)
                    {
                        var pad = ReadByteSkippingCrLf(inputStream);
                        if (pad == -1 || pad != '=') throw new InvalidPaddingException();

                        quantum[qpos++] = 0x00; // set last bytes of quatum to zero
                    }

                    quantum[qpos++] = 0x00;
                    Debug.Assert(qpos == 8);

                    // still not the end
                    if (ReadByteSkippingCrLf(inputStream) != -1)
                        throw new InvalidPaddingException();

                    DecodeQuantum(quantum, dquantum);
                    outputStream.Write(dquantum, 0, dlen);

                    break;
                }
                else
                {
                    ch = DECODE_TABLE[ch];
                    if (ch == INVALID_CHARACTER) throw new InvalidCharacterException();

                    quantum[qpos++] = ch;
                    if (qpos == 8)
                    {
                        DecodeQuantum(quantum, dquantum);
                        outputStream.Write(dquantum, 0, 5);
                        qpos = 0;
                    }
                }

                // it's time to check cancellation and report progress
                spos++;
                if (spos%(1024*100) == 0)
                {
                    ct.ThrowIfCancellationRequested();
                    pr.Report(spos);
                }
            }

            outputStream.Flush();
            pr.Report(pr.MaxValue);
        }


        public static void Decode(Stream inputStream, Stream outputStream,
                                  CancellationToken ct = default (CancellationToken),
                                  ProgressReporter pr = null)
        {
            if (inputStream == null)
                throw new ArgumentNullException("inputStream");

            if (outputStream == null)
                throw new ArgumentNullException("outputStream");

            if (!inputStream.CanRead)
                throw new ArgumentException("Input stream must be readable");

            if (!inputStream.CanSeek)
                throw new ArgumentException("Input stream must be seekable");

            if (!outputStream.CanWrite)
                throw new ArgumentException("Input stream must be writeable");

            pr = pr ?? ProgressReporter.None;

            var bufferedInput = new BufferedStream(inputStream, 800*1024);
            var bufferedOuptut = new BufferedStream(outputStream, 500*1024);

            DecodeInternal(bufferedInput, bufferedOuptut, ct, pr);
        }
    }
}